package com.xinxin.socket.msg;

import com.xinxin.socket.constant.CommandId;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCountUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.util.Arrays;

@Setter
@Accessors(chain = true)
public final class NetMsg {

    // 头长度/客户端版本号/类型/序列号，消息体长度
    private static final int FIXED_HEADER_SKIP = 4 + 4 + 4 + 4 + 4;
    private static final int CLIENT_VERSION = 0;

    @Getter
    private int headLength;
    @Getter
    private int clientVersion;
    @Getter
    private int cmdId;
    @Getter
    private int seq;
    @Getter
    private int bodyLen;

    private ByteBuf option;
    private ByteBuf body;

    public static class InvalidHeaderException extends Exception {

        private static final long serialVersionUID = 89489127207859635L;

        private InvalidHeaderException(String message) {
            super(message);
        }
    }

    public int getOptionLen() {
        return headLength - FIXED_HEADER_SKIP;
    }

    public NetMsg option(byte[] option) {
        this.option = Unpooled.wrappedBuffer(option);
        return this;
    }

    public NetMsg body(byte[] body) {
        this.body = Unpooled.directBuffer().writeBytes(body);
        return this;
    }

    public byte[] getOptionBytes() {
        return getBytes(option);
    }

    public byte[] getBodyBytes() throws InvalidHeaderException {
        if (clientVersion != CLIENT_VERSION) {
            throw new InvalidHeaderException("invalid client version in header, clientVersion: " + clientVersion);
        }

        if (body == null && cmdId != CommandId.SYS_HEART_PING) {
            throw new InvalidHeaderException("invalid header body, cmdId: " + cmdId);
        }

        return getBytes(body);
    }

    private byte[] getBytes(ByteBuf in) {
        if (in == null) {
            return null;
        }

        if (in.hasArray()) {
            byte[] array = in.array();
            int offset = in.arrayOffset() + in.readerIndex();
            return Arrays.copyOfRange(array, offset, in.readableBytes());
        } else {
            byte[] array = new byte[in.readableBytes()];
            in.getBytes(in.readerIndex(), array);
            return array;
        }
    }

    public void encode(ByteBuf out) throws InvalidHeaderException {
        if (body == null && cmdId != CommandId.SYS_HEART_REPLY) {
            throw new InvalidHeaderException("invalid header body");
        }

        headLength = FIXED_HEADER_SKIP + (option == null ? 0 : option.readableBytes());
        bodyLen = body == null ? 0 : body.readableBytes();

        out.writeInt(headLength);
        out.writeInt(CLIENT_VERSION);
        out.writeInt(cmdId);
        out.writeInt(seq);
        out.writeInt(bodyLen);

        if (option != null) {
            option.markReaderIndex();
            out.writeBytes(option);
            option.resetReaderIndex();
        }

        if (body != null) {
            body.getBytes(0, out, out.writerIndex(), body.readableBytes());
            out.writerIndex(out.writerIndex() + body.readableBytes());
        }
    }

    public int refCnt() {
        if (option == null && body == null) {
            return -1;
        }

        return option == null ? body.refCnt() : option.refCnt();
    }

    public void retain() {
        ReferenceCountUtil.retain(option);
        ReferenceCountUtil.retain(body);
    }

    public void release() {
        ReferenceCountUtil.release(option);
        ReferenceCountUtil.release(body);
    }

    @Override
    public String toString() {
        return "NetMsg{" +
                "headLength=" + headLength +
                ", clientVersion=" + clientVersion +
                ", cmdId=" + cmdId +
                ", seq=" + seq +
                // ", option=" + Arrays.toString(getOptionBytes()) +
                ", bodyLen=" + bodyLen +
                '}';
    }

    public static NetMsg decode(InputStream in) throws Exception {
        DataInputStream dis = new DataInputStream(in);

        NetMsg netMsg = new NetMsg()
                .setHeadLength(dis.readInt())
                .setClientVersion(dis.readInt())
                .setCmdId(dis.readInt())
                .setSeq(dis.readInt())
                .setBodyLen(dis.readInt());

        if (netMsg.getClientVersion() != CLIENT_VERSION) {
            throw new InvalidHeaderException("invalid client version in header, clientVersion: " + netMsg.getClientVersion());
        }

        int optionLen = netMsg.getHeadLength() - FIXED_HEADER_SKIP;
        if (optionLen > 0) {
            byte[] array = new byte[optionLen];
            dis.readFully(array);
            netMsg.option(array);
        }

        if (netMsg.getBodyLen() > 0) {
            byte[] array = new byte[netMsg.getBodyLen()];
            dis.readFully(array);
            netMsg.body(array);
        } else if (netMsg.getCmdId() != CommandId.SYS_HEART_REPLY) {
            throw new InvalidHeaderException("invalid header body, cmdId: " + netMsg.getCmdId());
        }

        return netMsg;
    }

    public static byte[] encode(int cmdId, int seq, byte[] options, byte[] body) throws Exception {
        if (body == null && cmdId != CommandId.SYS_HEART_PING) {
            throw new InvalidHeaderException("invalid header body");
        }

        int headLength = FIXED_HEADER_SKIP + (options == null ? 0 : options.length);
        int bodyLen = body == null ? 0 : body.length;
        int packLen = headLength + bodyLen;

        ByteArrayOutputStream out = new ByteArrayOutputStream(packLen);
        DataOutputStream dos = new DataOutputStream(out);

        dos.writeInt(headLength);
        dos.writeInt(CLIENT_VERSION);
        dos.writeInt(cmdId);
        dos.writeInt(seq);
        dos.writeInt(bodyLen);

        if (options != null) {
            dos.write(options);
        }

        if (body != null) {
            dos.write(body);
        }

        return out.toByteArray();
    }
}
